今天要來介紹的是用 Swift 開發時所使用管理記憶體的方法,這個觀念其實滿重要的,為的是避免在開發時,寫出來的代碼,造成未知的記憶體洩漏 (Memory Leak),但由於此章節內容較多,所以預計分成三天來撰寫。
Swift 為現代高階語言,對於記憶體有一套管理方法,即 Automatic Reference Counting -- ARC 來追蹤跟管理 app 的內存使用狀況,在大多數情況下,開發者在開發時就不需多費心力在處理記憶體的部分。當不在需要這些實例時,ARC 會自動釋放類實例使用的內存。
但是,在少數情況下,ARC 需要有關代碼部分之間關係的更多信息,以便為開發者管理內存,開發者也是有責任需要了解記憶體的管理,否則容易造成記憶體洩漏 (memory leak)。
引用計數僅適用於類的實例。結構和列舉是值類型,而不是引用類型,並且不使用引用存儲和傳遞。
每次創建類的新實例時,ARC 都會分配一塊內存來存儲有關該實例的資料。此內存保存有關實例類型的資料,以及與該實例相關聯的任何存儲屬性的值。
另外,當不再需要實例時,ARC 釋放該實例使用的內存,以便可以將內存用於其他目的。這可確保類實例在不再需要時不會佔用內存空間。
但是,如果 ARC 要釋放仍在使用的實例,則將無法再訪問該實例的屬性,或者調用該實例的方法。實際上,如果我們嘗試訪問該實例,則 app 很可能會崩潰。
為了確保實例在仍然需要時不會消失,ARC 會跟踪當前引用每個類實例的屬性、常數或變數的數量。只要至少有一個對該實例的引用仍然存在,ARC 就不會解除分配實例。
為了實現這一點,無論何時將類實例分配給屬性、常數或變數,該屬性、常數或變數都會對實例進行強引用。該引用被稱為「強」引用,因為它保持牢牢地抓住該實例,並且只要該強引用仍然存在就不允許它被釋放。
用以下的範例來解釋 ARC 的運作,先簡單的定義名為 Person
的類,內部儲存一個 name
的常數:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
以上例子將 reference1
定義為 Person
的可選型別 (optional) 形式,則 reference1
初始值則為 nil
,最後一行為創建一個新實例,並給入參數值,則會印出類被初始化。
接著我們再將定義一個也是為 Person
可選型別形式的變數,並將 reference1
指定給這個變數,接著將 reference1
指定為 nil
:
var reference2: Person?
reference2 = reference1
reference1 = nil
此時,Person
的實例尚未被釋放掉,最後再將 reference2
也指定為 nil
時,實例的記憶體才會被釋放掉:
reference2 = nil
// Prints "John Appleseed is being deinitialized"
在上述的範例中,ARC 能追蹤我們創建的新 Person
實例的引用數目,並在不再需要時釋放該 Person
實例。
但是,可以編寫一個代碼,其中類的實例永遠不會為零的強引用程度。如果兩個類實例彼此擁有強引用,這樣每個實例都會將另一個實例保持存活狀態。這被稱為強引用循環 (Strong Reference Cycles)。
可以通過將類之間的某些關係定義為弱引用 (weak references) 或無主引用 (unowned references) 而不是強引用來解決強引用循環。
以下為強引用循環的例子,這個例子定義了兩個名為 Person
和 Apartment
的類,它們模擬了一套公寓及其居民:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每一個 Person
的實例都擁有一個型別為 String
的屬性 name
和一個初始值為 nil
屬性為 apartment
的可選型別。
同樣的,每一個 Apartment
的實例中有一個型別為 String
的屬性 unit
和一個初始值為 nil
的屬性 tenant
可選型別。租戶 (tenant) 屬性是可選型別,因為公寓可能並不是都有租戶。
這兩個類還定義了一個反初始化器,它印出該類的一個實例被取消初始化的事實。這使我們可以查看 Person
和 Apartment
的實例是否按預期釋放。
接著將兩個變數指定為 Person
的可選型別和Apartment
的可選型別,因可選型別的值可設為 nil
,並帶入初始值:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
下圖解釋了創建和分配這兩個實例後強引用的狀況,變數 john
對新 Person
實例的強引用,及變數 unit4A
對新 Apartment
實例的強引用。
接著將 john
的 apartment
指定為 unit4A
,和把 unit4A
的 tenant
指定為 john
:
john!.apartment = unit4A
unit4A!.tenant = john
此時兩個實例連接在一起後強引用的情形:
不幸的是,連接這兩個實例會在它們之間產生強大的參考循環。 Person
實例現在具有對 Apartment
實例的強引用,並且 Apartment
實例具有對 Person
實例的強引用。因此,當我們中斷 john
和 unit4A
變量所持有的強引用時,引用計數不會降為零,並且 ARC 不會釋放實例:
john = nil
unit4A = nil
請注意,當我們將這兩個變數設置為 nil
時,都不會調用反初始化器。強引用循環可防止 Person
和 Apartment
實例被解除分配記憶體,進而導致 app 內存洩漏。